/*
 * This file is part of aion-engine <aion-engine.com>
 *
 * aion-engine is private software: you can redistribute it and or modify
 * it under the terms of the GNU Lesser Public License as published by
 * the Private Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * aion-engine is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser Public License for more details.
 *
 * You should have received a copy of the GNU Lesser Public License
 * along with aion-engine.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.aionengine.gameserver.spawnengine;

import com.aionengine.gameserver.ai2.AI2Logger;
import com.aionengine.gameserver.ai2.AIState;
import com.aionengine.gameserver.ai2.AISubState;
import com.aionengine.gameserver.ai2.NpcAI2;
import com.aionengine.gameserver.ai2.manager.WalkManager;
import com.aionengine.gameserver.model.gameobjects.Npc;
import com.aionengine.gameserver.model.templates.spawns.SpawnTemplate;
import com.aionengine.gameserver.model.templates.zone.Point2D;
import org.apache.commons.lang.ArrayUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.List;

import static ch.lambdaj.Lambda.*;

/**
 * @author vlog
 * @modified Rolandas
 */
public class WalkerGroup {

    private static final Logger log = LoggerFactory.getLogger(WalkerGroup.class);

    private List<ClusteredNpc> members;
    private WalkerGroupType type;
    private final float walkerXpos;
    private final float walkerYpos;
    private int[] memberSteps;
    private volatile int groupStep;
    private final String versionId;
    private boolean isSpawned;

    public WalkerGroup(List<ClusteredNpc> members) {
        this.members = sort(members, on(ClusteredNpc.class).getWalkerIndex());
        memberSteps = new int[members.size()];
        walkerXpos = members.get(0).getX();
        walkerYpos = members.get(0).getY();
        type = members.get(0).getWalkTemplate().getType();
        versionId = members.get(0).getWalkTemplate().getVersionId();
    }

    public void form() {
        if (getWalkType() == WalkerGroupType.SQUARE) {
            int[] rows = members.get(0).getWalkTemplate().getRows();
            if (sum(ArrayUtils.toObject(rows), on(Integer.class)) != members.size()) {
                log.warn("Invalid row sizes for walk cluster " + members.get(0).getWalkTemplate().getRouteId());
            }
            if (rows.length == 1) {
                // Line formation: distance 2 meters from each other (divide by 2 and multiple by 2)
                // negative at left hand and positive at the right hand
                float bounds = sum(members, on(ClusteredNpc.class).getNpc().getObjectTemplate().getBoundRadius().getSide());
                float distance = (1 - members.size()) / 2f * (WalkerGroupShift.DISTANCE + bounds);
                Point2D origin = new Point2D(walkerXpos, walkerYpos);
                Point2D destination = new Point2D(members.get(0).getWalkTemplate().getRouteStep(2).getX(), members.get(0).getWalkTemplate()
                        .getRouteStep(2).getY());
                for (int i = 0; i < members.size(); i++, distance += WalkerGroupShift.DISTANCE) {
                    WalkerGroupShift shift = new WalkerGroupShift(distance, 0);
                    Point2D loc = getLinePoint(origin, destination, shift);
                    members.get(i).setX(loc.getX());
                    members.get(i).setY(loc.getY());
                    Npc member = members.get(i).getNpc();
                    member.setWalkerGroup(this);
                    member.setWalkerGroupShift(shift);
                    // distance += npc.getObjectTemplate().getBoundRadius().getSide();
                }
            } else if (rows.length != 0) {
                float rowDistances[] = new float[rows.length - 1];
                float coronalDist = 0;
                for (int i = 0; i < rows.length - 1; i++) {
                    if (rows[i] % 2 != rows[i + 1] % 2)
                        rowDistances[i] = 0.86602540378443864676372317075294f * WalkerGroupShift.DISTANCE;
                    else
                        rowDistances[i] = WalkerGroupShift.DISTANCE;
                    coronalDist -= rowDistances[i];
                }
                Point2D origin = new Point2D(walkerXpos, walkerYpos);
                Point2D destination = new Point2D(members.get(0).getWalkTemplate().getRouteStep(2).getX(), members.get(0).getWalkTemplate()
                        .getRouteStep(2).getY());
                int index = 0;
                for (int i = 0; i < rows.length; i++) {
                    float sagittalDist = (1 - rows[i]) / 2f * WalkerGroupShift.DISTANCE;
                    for (int j = 0; j < rows[i]; j++, sagittalDist += WalkerGroupShift.DISTANCE) {
                        if (index > members.size() - 1)
                            break;
                        WalkerGroupShift shift = new WalkerGroupShift(sagittalDist, coronalDist);
                        Point2D loc = getLinePoint(origin, destination, shift);
                        ClusteredNpc cnpc = members.get(index++);
                        cnpc.setX(loc.getX());
                        cnpc.setY(loc.getY());
                        cnpc.getNpc().setWalkerGroup(this);
                        cnpc.getNpc().setWalkerGroupShift(shift);
                    }
                    if (i < rows.length - 1)
                        coronalDist += rowDistances[i];
                }
            }
        } else if (getWalkType() == WalkerGroupType.CIRCLE) {
            // TODO: if needed
        } else if (getWalkType() == WalkerGroupType.POINT) {
            log.warn("No formation specified for walk cluster " + members.get(0).getWalkTemplate().getRouteId());
        }
    }

    @SuppressWarnings("unused")
    private float getSidesExtra(int[] rows, int startIndex, int endIndex) {
        return 0;
    }

    /**
     * Returns coordinates of NPC in 2D from the initial spawn location
     *
     * @param origin      - initial spawn location
     * @param destination - point of next move
     * @param shift       - distance from origin located in lines perpendicular to destination; for SagittalShift if negative then
     *                    located to the left from origin, otherwise, to the right for CoronalShift if negative then located to
     *                    back, otherwise to the front
     * @category TODO: move to MathUtil when all kinds of WalkerGroupType are implemented.
     */
    public static Point2D getLinePoint(Point2D origin, Point2D destination, WalkerGroupShift shift) {
        // TODO: implement angle shift
        WalkerGroupShift dir = getShiftSigns(origin, destination);
        Point2D result = null;
        if (origin.getY() - destination.getY() == 0) {
            return new Point2D(origin.getX() + dir.getCoronalShift() * shift.getCoronalShift(), origin.getY() - dir.getSagittalShift()
                    * shift.getSagittalShift());
        } else if (origin.getX() - destination.getX() == 0) {
            return new Point2D(origin.getX() + dir.getCoronalShift() * shift.getSagittalShift(), origin.getY() + dir.getCoronalShift()
                    * shift.getCoronalShift());
        } else {
            double slope = (origin.getX() - destination.getX()) / (origin.getY() - destination.getY());
            double dx = Math.abs(shift.getSagittalShift()) / Math.sqrt(1 + slope * slope);
            if (shift.getSagittalShift() * dir.getCoronalShift() < 0)
                result = new Point2D((float) (origin.getX() - dx), (float) (origin.getY() + dx * slope));
            else
                result = new Point2D((float) (origin.getX() + dx), (float) (origin.getY() - dx * slope));
        }
        if (shift.getCoronalShift() != 0) {
            Point2D rotatedShift = null;
            if (shift.getSagittalShift() != 0) {
                rotatedShift = getLinePoint(origin, destination,
                        new WalkerGroupShift(Math.signum(shift.getSagittalShift()) * Math.abs(shift.getCoronalShift()), 0));
            } else {
                rotatedShift = getLinePoint(origin, destination, new WalkerGroupShift(Math.abs(shift.getCoronalShift()), 0));
            }

            // since it's rotated, and perpendicular, dx and dy are reciprocal when not rotated
            float dx = Math.abs(origin.getX() - rotatedShift.getX());
            float dy = Math.abs(origin.getY() - rotatedShift.getY());
            if (shift.getCoronalShift() < 0) {
                if (dir.getSagittalShift() < 0 && dir.getCoronalShift() < 0) {
                    result = new Point2D(result.getX() + dy, result.getY() + dx);
                } else if (dir.getSagittalShift() > 0 && dir.getCoronalShift() > 0) {
                    result = new Point2D(result.getX() - dy, result.getY() - dx);
                } else if (dir.getSagittalShift() < 0 && dir.getCoronalShift() > 0) {
                    result = new Point2D(result.getX() + dy, result.getY() - dx);
                } else if (dir.getSagittalShift() > 0 && dir.getCoronalShift() < 0) {
                    result = new Point2D(result.getX() - dy, result.getY() + dx);
                }
            } else {
                if (dir.getSagittalShift() < 0 && dir.getCoronalShift() < 0) {
                    result = new Point2D(result.getX() - dy, result.getY() - dx);
                } else if (dir.getSagittalShift() > 0 && dir.getCoronalShift() > 0) {
                    result = new Point2D(result.getX() + dy, result.getY() + dx);
                } else if (dir.getSagittalShift() < 0 && dir.getCoronalShift() > 0) {
                    result = new Point2D(result.getX() - dy, result.getY() + dx);
                } else if (dir.getSagittalShift() > 0 && dir.getCoronalShift() < 0) {
                    result = new Point2D(result.getX() + dy, result.getY() - dx);
                }
            }
        }
        return result;
    }

    /*
     * Return a normalized direction vector
     */
    private static WalkerGroupShift getShiftSigns(Point2D origin, Point2D destination) {
        float dx = Math.signum(destination.getX() - origin.getX());
        float dy = Math.signum(destination.getY() - origin.getY());
        return new WalkerGroupShift(dx, dy);
    }

    public void setStep(Npc member, int step) {
        int currentStep = 0;
        for (int i = 0; i < members.size(); i++) {
            if (memberSteps[i] > currentStep)
                currentStep = memberSteps[i];
            if (members.get(i).getNpc().equals(member)) {
                AI2Logger.info(members.get(i).getNpc().getAi2(), "Setting step to " + step);
                memberSteps[i] = step;
            }
        }
        if (step > currentStep || step == 1)
            groupStep = step;
    }

    public void targetReached(NpcAI2 npcAI) {
        synchronized (members) {
            npcAI.setSubStateIfNot(AISubState.WALK_WAIT_GROUP);
            boolean allArrived = true;
            for (ClusteredNpc snpc : members) {
                allArrived &= snpc.getNpc().getAi2().getSubState() == AISubState.WALK_WAIT_GROUP;
                if (!allArrived)
                    break;
            }

            for (int i = 0; i < members.size(); i++) {
                ClusteredNpc snpc = members.get(i);
                if ((memberSteps[i] == groupStep) && !allArrived) {
                    npcAI.getOwner().getMoveController().abortMove();
                    npcAI.setStateIfNot(AIState.WALKING);
                    npcAI.setSubStateIfNot(AISubState.WALK_WAIT_GROUP);
                    continue;
                }
                npcAI = (NpcAI2) (snpc.getNpc().getAi2());
                if (npcAI.getSubState() == AISubState.WALK_WAIT_GROUP)
                    WalkManager.targetReached(npcAI);
            }
        }
    }

    public boolean isSpawned() {
        return isSpawned;
    }

    public void spawn() {
        for (ClusteredNpc snpc : members) {
            float height = getHeight(snpc.getX(), snpc.getY(), snpc.getNpc().getSpawn());
            snpc.spawn(height);
        }
        isSpawned = true;
    }

    public void respawn(Npc npc) {
        for (int index = 0; index < members.size(); index++) {
            ClusteredNpc snpc = members.get(index);
            if (snpc.getWalkerIndex() == npc.getSpawn().getWalkerIndex() && snpc.getNpc().getNpcId() == npc.getNpcId()) {
                synchronized (members) {
                    snpc.setNpc(npc);
                    memberSteps[index] = 1;
                }
                break;
            }
        }
    }

    public void despawn() {
        for (ClusteredNpc snpc : members) {
            snpc.despawn();
            // reset positions
            form();
            for (int index = 0; index < memberSteps.length; index++)
                memberSteps[index] = 1;
            groupStep = 1;
        }
        isSpawned = false;
    }

    public ClusteredNpc getClusterData(Npc npc) {
        for (ClusteredNpc snpc : members) {
            if (snpc.getNpc().equals(npc))
                return snpc;
        }
        return null;
    }

    private float getHeight(float x, float y, SpawnTemplate template) {
        /*
		 * if (GeoService.getInstance().isGeoOn()) { return GeoService.getInstance().getZ(template.getWorldId(), x, y, z, );
		 * }
		 */
        return template.getZ();
    }

    public int getPool() {
        return members.size();
    }

    /**
     * @return the type
     */
    public WalkerGroupType getWalkType() {
        return type;
    }

    public boolean isLinearlyPositioned(Npc npc) {
        if (type != WalkerGroupType.SQUARE)
            return false;
        for (ClusteredNpc snpc : members) {
            if (snpc.getNpc().equals(npc))
                return snpc.getWalkTemplate().getRows().length == 1;
        }
        return false;
    }

    /**
     * @return the groupStep
     */
    public int getGroupStep() {
        return groupStep;
    }

    public String getVersionId() {
        return versionId;
    }

}
